package com.jzo2o.market.service.impl;

import cn.hutool.db.DbRuntimeException;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jzo2o.api.market.dto.request.CouponUseBackReqDTO;
import com.jzo2o.api.market.dto.request.CouponUseReqDTO;
import com.jzo2o.api.market.dto.response.AvailableCouponsResDTO;
import com.jzo2o.api.market.dto.response.CouponUseResDTO;
import com.jzo2o.common.expcetions.BadRequestException;
import com.jzo2o.common.expcetions.CommonException;
import com.jzo2o.common.expcetions.DBException;
import com.jzo2o.common.expcetions.ForbiddenOperationException;
import com.jzo2o.common.model.PageResult;
import com.jzo2o.common.utils.*;
import com.jzo2o.market.enums.ActivityStatusEnum;
import com.jzo2o.market.enums.CouponStatusEnum;
import com.jzo2o.market.mapper.CouponMapper;
import com.jzo2o.market.model.domain.Activity;
import com.jzo2o.market.model.domain.Coupon;
import com.jzo2o.market.model.domain.CouponWriteOff;
import com.jzo2o.market.model.dto.request.CouponOperationPageQueryReqDTO;
import com.jzo2o.market.model.dto.request.SeizeCouponReqDTO;
import com.jzo2o.market.model.dto.response.ActivityInfoResDTO;
import com.jzo2o.market.model.dto.response.CouponInfoResDTO;
import com.jzo2o.market.service.IActivityService;
import com.jzo2o.market.service.ICouponService;
import com.jzo2o.market.service.ICouponUseBackService;
import com.jzo2o.market.service.ICouponWriteOffService;
import com.jzo2o.market.utils.CouponUtils;
import com.jzo2o.mvc.utils.UserContext;
import com.jzo2o.mysql.utils.PageUtils;
import com.jzo2o.redis.utils.RedisSyncQueueUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

import static com.jzo2o.common.constants.ErrorInfo.Code.SEIZE_COUPON_FAILD;
import static com.jzo2o.market.constants.RedisConstants.RedisKey.*;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author itcast
 * @since 2023-09-16
 */
@Service
@Slf4j
public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> implements ICouponService {

    @Resource(name = "seizeCouponScript")
    private DefaultRedisScript<String> seizeCouponScript;

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private IActivityService activityService;

    @Resource
    private ICouponUseBackService couponUseBackService;

    @Resource
    private ICouponWriteOffService couponWriteOffService;

    @Override
    public List<CouponInfoResDTO> queryForList(Long lastId, Long userId, Integer status) {
        //查询
        //select * from coupon where status = 传入的状态 and user_id = 登录用户id and id < lastId order by id desc limit 10
        List<Coupon> list = this.lambdaQuery()
                .eq(Coupon::getStatus, status)
                .eq(Coupon::getUserId, userId)
                .lt(ObjectUtils.isNotNull(lastId), Coupon::getId, lastId)
                .orderByDesc(Coupon::getId)
                .last("limit 10")
                .list();
        // 数据转换
        return BeanUtils.copyToList(list, CouponInfoResDTO.class);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public CouponUseResDTO use(CouponUseReqDTO couponUseReqDTO) {
        //1. 校验优惠券信息: 只有订单金额大于等于满减金额，并且优惠券在有效状态方可使用
        Coupon coupon = this.lambdaQuery()
                .eq(Coupon::getUserId, UserContext.currentUserId()) //- 所属用户：当前登录用户
                .eq(Coupon::getStatus, CouponStatusEnum.NO_USE.getStatus())//- 状态：未使用
                .gt(Coupon::getValidityTime, LocalDateTime.now())//且在有效使用期限内
                .le(Coupon::getAmountCondition, couponUseReqDTO.getTotalAmount())// - 满减金额：小于等于订单总额
                .eq(Coupon::getId, couponUseReqDTO.getId())
                .one();
        if (ObjectUtils.isNull(coupon)) {
            throw new ForbiddenOperationException("优惠券不存在");
        }

        //2. 核销之后，需要根据优惠券id修改优惠券表中该优惠券的使用使用状态、使用时间、订单id等
        boolean update = lambdaUpdate()
                .eq(Coupon::getId, couponUseReqDTO.getId())
                .eq(Coupon::getStatus, CouponStatusEnum.NO_USE.getStatus())
                .gt(Coupon::getValidityTime, DateUtils.now())
                .le(Coupon::getAmountCondition, couponUseReqDTO.getTotalAmount())
                .set(Coupon::getOrdersId, couponUseReqDTO.getOrdersId())//订单id
                .set(Coupon::getStatus, CouponStatusEnum.USED.getStatus())//使用状态
                .set(Coupon::getUseTime, LocalDateTime.now())//使用时间
                .update();
        if (!update) {
            throw new DBException("优惠券核销失败");
        }

        //3. 向优惠券核销表添加记录
        CouponWriteOff couponWriteOff = CouponWriteOff.builder()
                .id(IdUtils.getSnowflakeNextId())
                .couponId(couponUseReqDTO.getId())
                .userId(UserContext.currentUserId())
                .ordersId(couponUseReqDTO.getOrdersId())
                .activityId(coupon.getActivityId())
                .writeOffTime(LocalDateTime.now())
                .writeOffManName(coupon.getUserName())
                .writeOffManPhone(coupon.getUserPhone())
                .build();
        if (!couponWriteOffService.save(couponWriteOff)) {
            throw new DBException("优惠券核销失败");
        }

        //4. 核销成功返回最终优惠的金额
        BigDecimal discountAmount = CouponUtils.calDiscountAmount(coupon, couponUseReqDTO.getTotalAmount());
        CouponUseResDTO couponUseResDTO = new CouponUseResDTO();
        couponUseResDTO.setDiscountAmount(discountAmount);
        return couponUseResDTO;
    }

    @Override
    public List<AvailableCouponsResDTO> getAvailable(BigDecimal totalAmount) {
        //1. 查询出满足条件的优惠券
        List<Coupon> list = this.lambdaQuery()
                .eq(Coupon::getUserId, UserContext.currentUserId()) //- 所属用户：当前登录用户
                .eq(Coupon::getStatus, CouponStatusEnum.NO_USE.getStatus())//- 状态：未使用
                .gt(Coupon::getValidityTime, LocalDateTime.now())//且在有效使用期限内
                .le(Coupon::getAmountCondition, totalAmount)// - 满减金额：小于等于订单总额
                .list();
        if (CollUtils.isEmpty(list)) {
            List.of();
        }

        //2. 组装返回数据
        return list.stream()
                .map(e -> e.setDiscountAmount(CouponUtils.calDiscountAmount(e, totalAmount))) //处理优惠金额
                .filter(e -> e.getDiscountAmount().compareTo(new BigDecimal(0)) > 0
                        && e.getDiscountAmount().compareTo(totalAmount) < 0)//过滤出 0< 过滤优惠金额 <订单金额
                .map(e -> BeanUtils.copyBean(e, AvailableCouponsResDTO.class))//转换为目标集合
                .sorted(Comparator.comparing(AvailableCouponsResDTO::getDiscountAmount).reversed())//按照优惠金额倒序排列
                .collect(Collectors.toList());//收集为集合
    }

    //抢券
    @Override
    public void seizeCoupon(SeizeCouponReqDTO seizeCouponReqDTO) {
        // 1.校验当前时间是否在活动期间
        ActivityInfoResDTO activity = activityService.getActivityInfoByIdFromCache(seizeCouponReqDTO.getId());
        if (activity == null || activity.getDistributeStartTime().isAfter(LocalDateTime.now())) {
            throw new CommonException(SEIZE_COUPON_FAILD, "活动不存在或者未开始");
        }
        if (activity.getDistributeEndTime().isBefore(LocalDateTime.now())) {
            throw new CommonException(SEIZE_COUPON_FAILD, "活动不存在或者已结束");
        }

        //2. 抢券
        //2-1 准备参数
        int index = (int) (seizeCouponReqDTO.getId() % 10);
        String couponSeizeSyncRedisKey = RedisSyncQueueUtils.getQueueRedisKey(COUPON_SEIZE_SYNC_QUEUE_NAME, index);// 同步队列redisKey
        String resourceStockRedisKey = String.format(COUPON_RESOURCE_STOCK, index);// 资源库存redisKey
        String couponSeizeListRedisKey = String.format(COUPON_SEIZE_LIST, activity.getId(), index);// 抢券列表
        log.debug("seize coupon keys -> couponSeizeListRedisKey->{},resourceStockRedisKey->{},couponSeizeListRedisKey->{},seizeCouponReqDTO.getId()->{},UserContext.currentUserId():{}",
                couponSeizeListRedisKey, resourceStockRedisKey, couponSeizeListRedisKey, seizeCouponReqDTO.getId(), UserContext.currentUserId());

        //2-2 执行抢券脚本
        Object executeResult = redisTemplate.execute(
                seizeCouponScript, //脚本
                Arrays.asList(couponSeizeSyncRedisKey, resourceStockRedisKey, couponSeizeListRedisKey),//键
                seizeCouponReqDTO.getId(), UserContext.currentUserId()//参数
        );

        //3. 返回结果
        if (executeResult == null) {
            throw new CommonException(SEIZE_COUPON_FAILD, "抢券失败");
        }
        long result = NumberUtils.parseLong(executeResult.toString());
        if (result > 0) {
            return; //成功
        } else if (result == -1) {
            throw new CommonException(SEIZE_COUPON_FAILD, "限领一张");
        } else if (result == -2 || result == -4) {
            throw new CommonException(SEIZE_COUPON_FAILD, "已抢光!");
        } else {
            throw new CommonException(SEIZE_COUPON_FAILD, "抢券失败");
        }
    }

    @Override
    public PageResult<CouponInfoResDTO> findByPage(CouponOperationPageQueryReqDTO dto) {
        if (ObjectUtils.isEmpty(dto.getActivityId())) {
            throw new ForbiddenOperationException("活动id不能为空");
        }
        Page<Coupon> page = this.lambdaQuery()
                .eq(Coupon::getActivityId, dto.getActivityId())
                .page(new Page<>(dto.getPageNo(), dto.getPageSize()));

        return PageUtils.toPage(page, CouponInfoResDTO.class);
    }

    @Override
    public void processExpireCoupon() {
        this.lambdaUpdate()
                .eq(Coupon::getStatus, CouponStatusEnum.NO_USE.getStatus())
                .le(Coupon::getValidityTime, DateUtils.now())
                .set(Coupon::getStatus, CouponStatusEnum.INVALID.getStatus())
                .update();
    }
}
